package com.fxu.framework.biz.aop;

import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONUtil;
import com.fxu.framework.biz.ano.Limit;
import com.fxu.framework.biz.ano.LimitType;
import com.fxu.framework.core.exception.MsgException;
import com.fxu.framework.core.holder.RedisHolder;
import com.fxu.framework.core.holder.RequestHolder;
import com.fxu.framework.core.util.MapUtil;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.RequestBody;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Map;

import static com.fxu.framework.core.holder.RequestHolder.getRequestIp;

/**
 * 方法限流
 * @author fangxu
 * @version 1.0.0
 */
@Slf4j
@Aspect
@Component
public class LimitAspect {

    @Pointcut("@annotation(com.fxu.framework.biz.ano.Limit)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = RequestHolder.getRequestMayNull();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Class<?> targetClass = signature.getDeclaringType();
        Method signatureMethod = signature.getMethod();
        String methodName = signatureMethod.getName();
        Limit limit = signatureMethod.getAnnotation(Limit.class);
        String key = limit.limitType() == LimitType.IP ? getRequestIp(request) : methodName;
        Object[] args = joinPoint.getArgs();
        Parameter[] parameters = signatureMethod.getParameters();
        Object requestBody = null;
        for (int i = 0; i < args.length; i++) {
            RequestBody annotation = parameters[i].getAnnotation(RequestBody.class);
            if (!ObjectUtils.isEmpty(annotation)) {
                requestBody = args[i];
                break;
            }
        }

        String uri = request == null ? "" : request.getRequestURI().replaceAll("/","_");
        Map<String, String[]> map = request == null ? null : request.getParameterMap();
        String params = request == null || MapUtil.isEmpty(map) ? "" : JSONUtil.toJsonStr(map);
        String body = ObjectUtils.isEmpty(requestBody) ? "" : JSONUtil.toJsonStr(requestBody);
        String other = SecureUtil.md5(params + body);
        ImmutableList<Object> keys = ImmutableList.of(uri + "_" + key + (limit.param() ?  "_" + other : ""));
        //ImmutableList<Object> keys = ImmutableList.of(StrUtil.join(limit.prefix(), "_", key, "_", uri));

        RedisScript<Long> redisScript = new DefaultRedisScript<>(buildLuaScript(), Long.class);
        Long count = RedisHolder.getRedisObjectTemplate().execute(redisScript, keys, limit.count(), limit.period());
        log.debug("{}.{} >>> {} < {} <<< {}", targetClass.getName(), methodName, count, limit.count(), keys);
        if (null != count && count.intValue() <= limit.count()) {
            return joinPoint.proceed();
        } else {
            uri = request == null ? "" : request.getRequestURI();
            log.debug("uri={}, params={}, body={}, md5={}", uri, params, body, other);
            throw new MsgException("限制了接口访问频率，请稍后再试");
        }
    }

    /**
     * 限流脚本
     */
    private String buildLuaScript() {
        return "local c" +
                "\nc = redis.call('get',KEYS[1])" +
                "\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
                "\nreturn c;" +
                "\nend" +
                "\nc = redis.call('incr',KEYS[1])" +
                "\nif tonumber(c) == 1 then" +
                "\nredis.call('expire',KEYS[1],ARGV[2])" +
                "\nend" +
                "\nreturn c;";
    }
}
